《Linux – Linux高级编程 – 第二部分 进程与线程》第3章 进程间通信(信号通信)

3.2信号通信

信号是在软件层次上对中断机制的一种模拟。在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的:一个进程不必通过任何操作在等待信号的到达。

事实上,进程也不知道信号到底什么时候到达。信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了那些系统事件。它可以在任何时候发给某一进程,而无需知道该进程的状态。如果该进程当前并未处于执行态,则该信号就由内核保存起来,知道该进程回恢复行再传递给它为止;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直道阻塞被取消时才被传递给进程。

3.2.1信号的生存周期

一个完整的信号生命周期分为三个阶段:
1.信号发生;
2.信号在进程中的注册;
3.信号的投递。

cDbA0A.png

术语 解释
信号投递(delivery) 实际执行信号的处理动作称为信号投递
信号未决(pending) 信号从的发生到投递之间的状态,称为信号未决
阻塞(block) 进程可以选择阻塞某个信号,被阻塞的信号产生后保持在未决状态,直到进程解除对某个信号的阻塞,才会执行投递的动作
信号部署(dispostition) 设置信号的处理策略

3.2.2进程响应信号的方式

1)忽略信号
即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及 SIGSTOP;

2)捕捉信号
定义信号处理函数,当信号发生时,执行相应的处理函数。

3)执行默认操作
Linux 对每种信号都规定了默认操作(后面会给出信号列表)。

这里介绍几个常用的信号。

信号名 含义 默认操作
SIGINT 该信号在用户输入INTR字符(通常是Ctrl+C)时发出,终端驱动程序发送该信号并送到前台进程中的每一个进程 终止进程
SIGQUIT 该信号和SIGINT类似,但由QUIT字符(通常是Ctrl+\)来控制 终止进程
SIGKILL 该信号用来立即结束程序的运行,不能被阻塞、处理和忽略 终止进程
SIGALARM 该信号当一个定时器道德时候发出 终止进程
SIGSTSOP 该信号用来暂停一个进程,不能被阻塞、处理和忽略 暂停进程
SIGTSTP 该信号用于交互停止进程(挂起),由Ctrl+Z来发出 终止进程

3.2.3信号处理流程

下面是内核如何实现信号机制,即内核如何向一个进程发送信号、进程如何接收一个信号、进程怎样控制自己对信号的反应、内核在什么实际处理和怎样处理进程收到的信号。

cDbwX4.png

内核给一个进程发送软中断信号的方法是,在进程所在的进程表项的信号域设置对于该信号的位(内核通过在进程的 struct task_struct 结构中的信号域中设置相应的位来实现向一个进程发送信号)。这里要补充的是,如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。这一点比较重要,因为进程检查是否收到信号的时机是:一个进程在即将从内核态返回到用户态时;或者,在一个进程要进入或离开一个适当的低调度优先级睡眠状态时。

内核处理一个进程收到的信号的时机是一个进程从内核态返回用户态时。所以,当一个进程在内核态运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。

内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户自定义的使用系统调用signal() 注册的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就像从来没有收到该信号似得,而继续运行。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的处理函数处,从函数返回再弹出栈顶时,才返回原来进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。

在信号的处理方法中有几点特别要引起注意:

在一些系统中,当一个进程处理完中断信号返回用户态之前,内核清除用户区中设定的对该信号的处理例程的地址,即下一次进程对该信号的处理方法又改为默认值,除非在下一次信号到来之前再次调用 signal() 系统调用。这可能会使得进程在调用 signal() 之前又得到该信号而导致退出。在BSD系统中,内核不再清除该地址。但不清楚该地址可能使得进程因为过多过快的得到某个信号而导致堆栈溢出。为了避免出现上述情况。在BSD中,内核模拟了对硬件中断的处理方法,即在处理某个中断时,阻止接收新的该类中断。

3.2.4信号相关函数

1)信号发送:kill() 和 raise()
kill() 函数同读者熟知的kill 系统命令一样,可以发送信号给进程或进程组(实际上,kill 系统命令就是由 kill () 函数实现的)。需要注意的是,kill函数它不仅可以终止进程,也可以向进程发送其他信号;
与kill() 函数不同的是,raise() 函数只允许进程向自身发送信号;

所需头文件 #include
#include <sys/types.h>
函数原型 int kill(pid_t pid, int sig);
函数传入值 pid为正数,发送进程号为pid的进程
pid为0:信号被发送到所有和当前进程在同一个进程组的进程
pid为-1:信号发送给所有进程表中的进程(除了进程号最大的进程外)
pid为<-1:信号发送给进程组为-pid的每一个进程
sig:信号类型
函数返回值 成功:0;失败:-1
所需头文件 #include
#include <sys/types.h>
函数原型 int raise( int sig);
函数传入值 sig:信号类型
函数返回值 成功:0;失败:-1

这里 raise() 等价于 kill ( getpid() , sig) ;

#include <stdio.h>  
#include <stdlib.h>  
#include <signal.h>  
#include <sys/types.h>  
#include <sys/wait.h>  
#include <unistd.h>  

int main(int argc, char *argv[])  
{  
    pid_t pid;  
    int ret;  

    if((pid = fork()) < 0)  
    {  
        perror("fork error");  
        exit(-1);  
    }  
    if(pid == 0)  
    {  
        printf("child(pid : %d)is waiting for any signal\n",getpid());  
        raise(SIGSTOP);  
        exit(0);  
    }  
    sleep(1);  
    if((waitpid(pid,NULL,WNOHANG)) == 0)  
    {  
        kill(pid,SIGKILL);  
        printf("parent kill child process %d\n",pid);     
    }  
    waitpid(pid,NULL,0);  
    return 0;  
}  

执行结果如下:

cDq9Nq.png

2)、定时器信号:alarm() 、pause()
alarm() 也称闹钟信号,它可以在进程中设置一个定时器。默认动作是终止信号。当定时器指定的时间到时,它就向进程发送SIGALRAM信号。要注意的是,一个进程只能有一个闹钟时间,如果在调用alarm()函数之前已设置过闹钟信号,则任何以前的闹钟时间都被新值所代替。

pause()函数是用于将调用进程挂起直至收到信号为止,挂起才结束。

所需头文件 #include
函数原型 unsigned int alarm(unsigned int second);
函数传入值 second:指定秒数,系统经过seconds秒之后向该进程发送SIGALARM信号
函数返回值 成功:如果调用alarm()前,进程中已经设置了闹钟时间,则返回上一个时间剩余的时间,否则返回0;
失败:-1
所需头文件 #include
函数原型 int pause(void);
函数返回值 -1,并把errno值设为RINTR
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

int main(int argc,char **argv)
{
    int ret;
    //调用alarm()定时器函数
    alarm(5);
    pause();
    printf("I have been waken up.\n",ret);
}

执行结果:

cDqZDJ.png

上面的代码中最后一句是不会执行的,因为对于pause()函数,当收到一个信号来时,进程的挂起会结束进程也会结束,一般和定时器alarm()同时使用,另外在调试代码中,为了看到运行的结果,就是放在程序运行的最后,直到收到结束信号(Ctrl+C)。

3)、信号的设置 signal() 和 sigaction()

signal() 函数

要对一个信号进行处理,就需要给出此信号发生时系统所调用的处理函数。可以为一个特定的信号(除去无法捕捉的SIGKILL和SIGSTOP信号)注册相应的处理函数。如果正在运行的程序源代码里注册了针对某一特定信号的处理程序,不论当时程序执行到何处,一旦进程接收到该信号,相应的调用就会发生。

signal()函数使用时,只需要指定的信号类型和信号处理函数即可。它主要用于前32种非实时信号的处理,不支持信号传递信息。

所需头文件 #include
函数原型 typedef void (*sighandler_t)(int);函数指针类型
sighandler_t signal(int signum, sighandler_t handler);
函数传入值 signum:指定信号代码
handler:SIG_IGN忽略该信号,SIG_DFL采用系统默认方式处理信号,自定义信号类型
函数返回值 成功:以前的信号处理函数
失败:-1

该函数第二个参数和返回值类型都是指向一个无返回值并且带一个整型参数的函数的指针;且只要signal() 调用了自定义的信号处理函数,即使这个函数什么也不做,这个进程也不会被终止。

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

void my_fun(int sign_no)
{
    if(sign_no == SIGINT)
    {
        printf("I have get SIGINT!\n");
    }
    else if(sign_no == SIGQUIT)
    {
        printf("I have get SIGQUIT!\n");
    }
}

int main ( void ) 
{
    printf("waitting singal SIGINT SIGQUIT!\n");

    //注册信号处理函数
    signal(SIGINT,my_fun);
    signal(SIGQUIT,my_fun);

    pause();
    exit(0);
}

结果如下所示:

cDqUUI.png

下面一个程序利用signal来实现发送信号和接受信号的原理:

程序内容:创建子进程代表售票员,父进程代表司机,同步过程如下:
售票员捕捉 SIGINT(代表开车),发送信号SIGUSR1给司机,司机打印(“let’s gogogo!”);
售票员捕捉 SIGQUIT(代表停止),发送信号SIGUSR2给司机,司机打印(“stop the bus!”);
司机捕捉 SIGTSTP (代表车到总站),发SIGUSR1给售票员,售票员打印(“Please get off the bus”);

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <signal.h>  
#include <sys/types.h>  
pid_t pid;  
void driver_handler(int signo);  
void saler_handler(int signo);  
int main(int argc,char *argv[])  
{  
    if((pid = fork()) < 0)  
    {  
        perror("fork error");  
        return -1;  
    }  
    if(pid > 0)  
    {  
        signal(SIGTSTP,driver_handler);  
        signal(SIGINT,SIG_IGN);  
        signal(SIGQUIT,SIG_IGN);  
        signal(SIGUSR1,driver_handler);  
        signal(SIGUSR2,driver_handler);  
        while(1)  
            pause();  
    }  
    if(pid == 0)  
    {  
        signal(SIGINT,saler_handler);  
        signal(SIGTSTP,SIG_IGN);  
        signal(SIGQUIT,saler_handler);  
        signal(SIGUSR1,saler_handler);  
        signal(SIGUSR2,SIG_IGN);  
        while(1)  
            pause();  
    }  
    return 0;  
}  
void driver_handler(int signo)  
{  
    if(signo == SIGUSR1)  
        printf("Let's gogogo!\n");  
    if(signo == SIGUSR2)  
        printf("Stop the bus!\n");  
    if(signo == SIGTSTP)  
        kill(pid,SIGUSR1);  
}  
void saler_handler(int signo)  
{  
    pid_t ppid = getppid();  
    if(signo == SIGINT)  
        kill(ppid,SIGUSR1);  
    if(signo == SIGQUIT)  
        kill(ppid,SIGUSR2);  
    if(signo == SIGUSR1)  
    {  
        printf("please get off the bus\n");  
        kill(ppid,SIGKILL);  
        exit(0);  
    }  
}  

执行结果如下:

cDqB28.png

signal()和alarm()函数可以一起使用,用于定时处理一些事件,看个例子吧。

#include<unistd.h>
#include<stdio.h>
#include<signal.h>

void handler()
{
    printf("hello\n");
}

int main(void)
{
    int i;
    signal(SIGALRM, handler);
    alarm(5);

    for(i=1;i<7;i++)
{
        printf("sleep %d....\n",i);
        sleep(1);
    }

    return 0;
}

结果如下所示:

cDqfP0.png

sigaction() 函数
sigaction() 函数的功能是检查或修改(或两者)与指定信号相关联的处理动作,此函数可以完全代替signal 函数。

所需头文件 #include
函数原型 int sigaction(int signum, const struct sigaction act, struct sigaction oldact);
函数传入值 signum:指定SIGKILL和SIGSTOP以外的所有信号
act:act是一个结构体,里面包含信号处理的地址、处理方式等
oldact:参数oldact是一个传出参数,sigaction函数调用成功给以后,oldact里面包含以前对signum信号的处理方式的信息
函数返回值 成功:0
失败:-1

其中参数signo 是要检测或修改其具体动作的信号编号。若act 指针非NULL,则要修改其动作。如果oact 指针非空,则系统经由 oact 指针返回该信号的上一个动作;

参数结构sigaction定义如下:

struct sigaction  
{  
    void (*sa_handler) (int);  
    void (*sa_sigaction)(int, siginfo_t *, void *);  
    sigset_t sa_mask;  
    int sa_flags;  
    void (*sa_restorer) (void);  
} 

① sa_handler:此参数和signal()的参数handler相同,此参数主要用来对信号旧的安装函数signal()处理形式的支持;
② sa_sigaction:新的信号安装机制,处理函数被调用的时候,不但可以得到信号编号,而且可以获悉被调用的原因以及产生问题的上下文的相关信息。
③ sa_mask:用来设置在处理该信号时暂时将sa_mask指定的信号搁置;
④ sa_restorer: 此参数没有使用;
⑤ sa_flags:用来设置信号处理的其他相关操作,下列的数值可用。可用OR 运算(|)组合:
SA_NOCLDSTOP:如果参数signum为SIGCHLD,则当子进程暂停时并不会通知父进程
SA_ONESHOT/SA_RESETHAND:当调用新的信号处理函数前,将此信号处理方式改为系统预设的方式
SA_RESTART:被信号中断的系统调用会自行重启
SA_NOMASK/SA_NODEFER:在处理此信号未结束前不理会此信号的再次到来
SA_SIGINFO:信号处理函数是带有三个参数的sa_sigaction。

4)、信号集处理相关函数

#include <unistd.h>  
#include <signal.h>  
#include <sys/types.h>  
#include <stdlib.h>  
#include <stdio.h>  

void handler(int sig)  
{  
    printf("Handler the signal %d\n", sig);  
}  

int main(void)  
{  
    sigset_t sigset;//用于记录屏蔽字  
    sigset_t ign;//用于记录被阻塞的信号集  
    struct sigaction act; 

    //清空信号集  
    sigemptyset(&sigset);  //初始化信号集
    sigemptyset(&ign);  
    //向信号集中添加信号SIGINT  
    sigaddset(&sigset, SIGINT);  

    //设置处理函数和信号集      
    act.sa_handler = handler;  
    sigemptyset(&act.sa_mask);  
    act.sa_flags = 0;  
    sigaction(SIGINT, &act, 0);  

    printf("Wait the signal SIGINT...\n");  
    pause();//挂起进程,等待信号  

    //设置进程屏蔽字,在本例中为屏蔽SIGINT   
    sigprocmask(SIG_SETMASK, &sigset, 0);     
    printf("Please press Ctrl+c in 10 seconds...\n");  
    sleep(10); 

    //测试SIGINT是否被屏蔽  
    sigpending(&ign);  

    if(sigismember(&ign, SIGINT))  
        printf("The SIGINT signal has ignored\n"); 

    //在信号集中删除信号SIGINT  
    sigdelset(&sigset, SIGINT);  
    printf("Wait the signal SIGINT...\n"); 

    //将进程的屏蔽字重新设置,即取消对SIGINT的屏蔽  
    //并挂起进程  
    sigsuspend(&sigset);  

    printf("The app will exit in 5 seconds!\n");  
    sleep(5);  
    exit(0);  
}

结果如下所示:

cDqXPx.png

Related posts

Leave a Comment